package bsh;

import java.lang.reflect.Array;

import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import bsh.asm.acessor.FieldObjectAcessor;
import bsh.asm.acessor.LocalObjectAccessor;
import bsh.asm.acessor.ObjectAccessor;
import bsh.exception.CompilationAssertionFailed;
import bsh.exception.CompilationFailedException;
import bsh.meta.ArrayTypeWrapper;
import bsh.meta.FieldDescriptor;
import bsh.meta.JavaClassWrapper;
import bsh.meta.JavaMethodDescriptor;
import bsh.meta.MethodDescriptor;
import bsh.meta.Scope;
import bsh.meta.TypeWrapper;
import bsh.utils.reflection.BytecodeHelper;

/**
 * Compiler para um assignment
 * 
 * @author takeshi
 */
public class AssignmentNodeCompiler implements NodeCompiler {

    private static final int NUM_3 = 3;

    private final BSHAssignment node;

    private final CompilationUnit unit;

    public static final MethodDescriptor ARRAY_ACCESS_MD, ARRAY_SET_MD;
    static {
        try {
            ARRAY_ACCESS_MD = new JavaMethodDescriptor(Array.class.getMethod("get", Object.class, int.class));
            ARRAY_SET_MD = new JavaMethodDescriptor(Array.class.getMethod("set", Object.class, int.class, Object.class));
        } catch (NoSuchMethodException e) {
            throw new ExceptionInInitializerError();
        }
    }

    /**
     * @param node {@link BSHAssignment}
     * @param unit {@link CompilationUnit}
     */
    public AssignmentNodeCompiler(BSHAssignment node, CompilationUnit unit) {
        this.node = node;
        this.unit = unit;
    }

    /**
     * {@inheritDoc}
     */
    public void compile(MethodVisitor host, CompilationState state) throws CompilationFailedException {
        BSHPrimaryExpression exp = (BSHPrimaryExpression) this.node.getChild(0);
        state.addLineNumber(host, exp.getLineNumber());
        final Scope currentScope = state.currentScope();
        TypeWrapper varType = this.nodeResultType(currentScope);
        if (varType == TypeWrapper.NULL_TYPE_WRAPPER) {
            varType = JavaClassWrapper.OBJECT_WRAPPER;
        }
        int children = exp.jjtGetNumChildren();
        if (this.isUndeclaredLocalVariable(exp, currentScope)) {
            Label start = new Label();
            LocalVariable lv = currentScope.addLocalVariable(((BSHAmbiguousName) exp.getChild(0)).text, start, varType);
            if (state.getUnit().options().initializeLocalVariables()) {
                // will initialize before assignment
                TypedVariableDeclarationCompiler.initializeWithDefaultValue(varType, host, state, lv.getName(), lv);
            }
            host.visitLabel(start);
        } else {
            for (int i = 0; i < children - 1; i++) {
                this.unit.getNodeCompiler(exp.getChild(i)).compile(host, state);
            }
        }
        SimpleNode lhs = exp.getChild(children - 1);
        AssignmentTarget target = createAssignmentTargetFor(lhs, state, this.unit);
        final SimpleNode rhsNode = this.node.getChild(1);
        if (this.node.operator == BSHAssignment.ASSIGN) {
            target.prepare(host, state);
            state.addLineNumber(host, rhsNode.getLineNumber());
            this.unit.getNodeCompiler(rhsNode).compile(host, state);
            this.unit.getCompilerManager().coerceStackTopTo(host, target.type(), state);
            LocalVariable tmpCopy = copyToLocalVariable(host, state);
            target.doAssign(host, state);
            if (target.isLocal()) {
                AmbiguousNameAssignmentTarget tgt = (AmbiguousNameAssignmentTarget) target;
                LocalObjectAccessor acc = (LocalObjectAccessor) tgt.getList().getFirst();
                LocalVariable variable = acc.getLocalVariable();
                TypedVariableDeclarationCompiler.setLocalVariable(target.type(), host, state, variable);
            }
            restoreFromLocalVariable(host, state, tmpCopy);
            return;
        }

        target.prepare(host, state);
        target.dup(host, state);
        target.load(host, state);
        state.addLineNumber(host, rhsNode.getLineNumber());
        TypeWrapper operandType = target.type();
        this.unit.getNodeCompiler(rhsNode).compile(host, state);
        state.assertMinimumStackSize(2);
        LocalVariable tmp = copyToLocalVariable(host, state);
        if (this.requiresEqualsTypes(this.node.operator)) {
            this.unit.getCompilerManager().coerceStackTopTo(host, operandType, state);
            state.assertStackTopType(operandType);
        } else if (operandType.isPrimitive()) {
            this.unit.getCompilerManager().coerceStackTopTo(host, JavaClassWrapper.wrapperFor(int.class), state);
        }
        if (!operandType.isPrimitive()) {
            this.unit.getCompilerManager().coerceStackTopTo(host, JavaClassWrapper.OBJECT_WRAPPER, state);
            MethodDescriptor md = this.unit.reflectionUtils().getMethodWithName(CompiledScriptHelper.class, "performBinaryOperation");
            BytecodeHelper.pushIntConst(this.unit.thisType().newDynamicInvocationId(), host);
            state.pushStack(int.class);
            BytecodeHelper.pushIntConst(this.findKindForBinaryOperation(), host);
            state.pushStack(int.class);
            UnaryOperationNodeCompiler.pushThis(host, state);
            md.writeInvoke(host, state);

            unit.getCompilerManager().coerceStackTopTo(host, operandType, state);

            target.doAssign(host, state);
            // state.popStack();
        } else {
            int opcode = this.selectOpcode(host, state, operandType);
            state.assertMinimumStackSize(1);
            host.visitInsn(opcode);
            state.popStack();
            target.doAssign(host, state);
        }
        if (target.isLocal()) {
            AmbiguousNameAssignmentTarget tgt = (AmbiguousNameAssignmentTarget) target;
            LocalObjectAccessor acc = (LocalObjectAccessor) tgt.getList().getFirst();
            LocalVariable variable = acc.getLocalVariable();
            TypedVariableDeclarationCompiler.setLocalVariable(target.type(), host, state, variable);
        }
        restoreFromLocalVariable(host, state, tmp);
    }

    /**
     * Restores a value from a local variable
     * 
     * @param host {@link MethodVisitor}
     * @param state {@link CompilationState}
     * @param tmp the {@link LocalVariable}
     */
    public void restoreFromLocalVariable(MethodVisitor host, CompilationState state, LocalVariable tmp) {
        host.visitVarInsn(tmp.getType().toASMType().getOpcode(Opcodes.ILOAD), tmp.getOffset());
        state.pushStack(tmp.getType());
    }

    /**
     * Copies the top of the stack to a local variable
     * 
     * @param host the {@link MethodVisitor}
     * @param state the {@link CompilationState}
     * @return {@link LocalVariable}
     */
    public LocalVariable copyToLocalVariable(MethodVisitor host, CompilationState state) {
        state.assertMinimumStackSize(1);
        LocalVariable tmpCopy = state.currentScope().addLocalVariable(null, state.currentScope().getStartLabel(), state.peekStack());
        this.dupStackTop(host, state);
        state.popStack();
        host.visitVarInsn(tmpCopy.getType().toASMType().getOpcode(Opcodes.ISTORE), tmpCopy.getOffset());
        return tmpCopy;
    }

    /**
     * Dups the stack top.
     * 
     * @param host {@link MethodVisitor}
     * @param state {@link CompilationState}
     */
    public void dupStackTop(MethodVisitor host, CompilationState state) {
        final TypeWrapper stackTop = state.peekStack();
        state.pushStack(stackTop);
        if (stackTop.numberOfSlots() == 2) {
            host.visitInsn(Opcodes.DUP2);
        } else {
            host.visitInsn(Opcodes.DUP);
        }
    }

    /**
     * Procura a operacao a ser executada
     * 
     * @return int
     */
    private int findKindForBinaryOperation() {
        switch (this.node.operator) {
        case BSHAssignment.PLUSASSIGN:
            return BSHBinaryExpression.PLUS;

        case BSHAssignment.MINUSASSIGN:
            return BSHBinaryExpression.MINUS;

        case BSHAssignment.ANDASSIGN:
        case BSHAssignment.ANDASSIGNX:
            return BSHBinaryExpression.BIT_AND;

        case BSHAssignment.STARASSIGN:
            return BSHBinaryExpression.STAR;

        case BSHAssignment.XORASSIGN:
            return BSHBinaryExpression.XOR;

        case BSHAssignment.SLASHASSIGN:
            return BSHBinaryExpression.SLASH;

        case BSHAssignment.MODASSIGN:
            return BSHBinaryExpression.MOD;

        case BSHAssignment.LSHIFTASSIGN:
        case BSHAssignment.LSHIFTASSIGNX:
            return BSHBinaryExpression.LSHIFT;

        case BSHAssignment.RSIGNEDSHIFTASSIGN:
        case BSHAssignment.RSIGNEDSHIFTASSIGNX:
            return BSHBinaryExpression.RSIGNEDSHIFT;

        case BSHAssignment.RUNSIGNEDSHIFTASSIGN:
        case BSHAssignment.RUNSIGNEDSHIFTASSIGNX:
            return BSHBinaryExpression.RUNSIGNEDSHIFT;

        default:
            throw new CompilationAssertionFailed("not yet implemented");
        }
    }

    /**
     * Seleciona o opcode
     * 
     * @param host {@link MethodVisitor}
     * @param state {@link CompilationState}
     * @param operandType {@link TypeWrapper}
     * @return int
     */
    private int selectOpcode(MethodVisitor host, CompilationState state, TypeWrapper operandType) {
        int opcode;
        switch (this.node.operator) {
        case BSHAssignment.PLUSASSIGN:
            opcode = operandType.toASMType().getOpcode(Opcodes.IADD);
            break;

        case BSHAssignment.MINUSASSIGN:
            host.visitInsn(state.peekStack().toASMType().getOpcode(Opcodes.INEG));
            opcode = operandType.toASMType().getOpcode(Opcodes.IADD);
            break;

        case BSHAssignment.ANDASSIGN:
        case BSHAssignment.ANDASSIGNX:
            opcode = operandType.toASMType().getOpcode(Opcodes.IAND);
            break;

        case BSHAssignment.ORASSIGN:
        case BSHAssignment.ORASSIGNX:
            opcode = operandType.toASMType().getOpcode(Opcodes.IOR);
            break;

        case BSHAssignment.STARASSIGN:
            opcode = operandType.toASMType().getOpcode(Opcodes.IMUL);
            break;

        case BSHAssignment.XORASSIGN:
            opcode = operandType.toASMType().getOpcode(Opcodes.IXOR);
            break;

        case BSHAssignment.SLASHASSIGN:
            opcode = operandType.toASMType().getOpcode(Opcodes.IDIV);
            break;

        case BSHAssignment.MODASSIGN:
            opcode = operandType.toASMType().getOpcode(Opcodes.IREM);
            break;

        case BSHAssignment.LSHIFTASSIGN:
        case BSHAssignment.LSHIFTASSIGNX:
            opcode = operandType.toASMType().getOpcode(Opcodes.ISHL);
            break;

        case BSHAssignment.RSIGNEDSHIFTASSIGN:
        case BSHAssignment.RSIGNEDSHIFTASSIGNX:
            opcode = operandType.toASMType().getOpcode(Opcodes.ISHR);
            break;

        case BSHAssignment.RUNSIGNEDSHIFTASSIGN:
        case BSHAssignment.RUNSIGNEDSHIFTASSIGNX:
            opcode = operandType.toASMType().getOpcode(Opcodes.IUSHR);
            break;

        default:
            throw new CompilationAssertionFailed("not yet implemented");
        }
        return opcode;
    }

    /**
     * @param operator int
     * @return boolean
     */
    private boolean requiresEqualsTypes(int operator) {
        return operator != BSHAssignment.LSHIFTASSIGN && operator != BSHAssignment.LSHIFTASSIGNX
                && operator != BSHAssignment.RSIGNEDSHIFTASSIGN && operator != BSHAssignment.RSIGNEDSHIFTASSIGNX
                && operator != BSHAssignment.RUNSIGNEDSHIFTASSIGN && operator != BSHAssignment.RUNSIGNEDSHIFTASSIGNX;
    }

    /**
     * cria um AssignmentTarget baseado no LHS
     * 
     * @param lhs node
     * @param state {@link CompilationState}
     * @param unit unit
     * @return {@link AssignmentTarget}
     * @throws CompilationFailedException e
     */
    public static AssignmentTarget createAssignmentTargetFor(SimpleNode lhs, CompilationState state, final CompilationUnit unit)
            throws CompilationFailedException {
        if (lhs instanceof BSHPrimaryExpression) {
            // extract the primary expression if solely
            if (lhs.jjtGetNumChildren() == 1) {
                return createAssignmentTargetFor(lhs.getChild(0), state, unit);
            } else {
                // complex expression i guess?
                return null;
                // complex expression... maybe (a + b) and the like.
            }
        } else if (lhs instanceof BSHAmbiguousName) {
            return new AmbiguousNameAssignmentTarget(lhs.getLineNumber(), ((BSHAmbiguousName) lhs).text, state, unit);
        } else if (lhs instanceof BSHPrimarySuffix) {
            final BSHPrimarySuffix suffix = (BSHPrimarySuffix) lhs;
            if (suffix.operation == BSHPrimarySuffix.NAME) {
                if (suffix.jjtGetNumChildren() != 0) {
                    throw new CompilationFailedException(lhs.getLineNumber(), "cannot assign to a method call");
                }
                TypeWrapper type = state.peekStack();
                final FieldDescriptor field = type.getField(suffix.field);
                if (field == null) {
                    throw new CompilationFailedException(lhs.getLineNumber(), "field: " + suffix.field + " not found on type: " + type);
                }
                return new FieldAssignmentTarget(field);
            } else if (suffix.operation == BSHPrimarySuffix.INDEX) {
                return new AssignmentTarget() {
                    private final CompilationUnit localUnit = unit;

                    private TypeWrapper type;

                    public void doAssign(MethodVisitor visitor, CompilationState state) throws CompilationFailedException {
                        state.assertMinimumStackSize(NUM_3);
                        TypeWrapper valueType = state.popStack();
                        TypeWrapper indexType = state.popStack();
                        TypeWrapper array = state.popStack();
                        if (!unit.reflectionUtils().isIntPrimitive(indexType)) {
                            throw new CompilationFailedException(suffix.getLineNumber(), "expected int but was: " + indexType);
                        }
                        if (!(array instanceof ArrayTypeWrapper)) {
                            state.pushStack(array);
                            state.pushStack(indexType);
                            state.pushStack(valueType);
                            unit.getCompilerManager().coerceStackTopTo(visitor, JavaClassWrapper.OBJECT_WRAPPER, state);
                            ARRAY_SET_MD.writeInvoke(visitor, state);
                        } else {
                            ArrayTypeWrapper concrete = (ArrayTypeWrapper) array;
                            // just for the type coercion
                            this.type = concrete.getComponentType();
                            state.pushStack(valueType);
                            unit.getCompilerManager().coerceToType(visitor, valueType, concrete.getComponentType(), state);
                            state.popStack();
                            visitor.visitInsn(concrete.getComponentType().toASMType().getOpcode(Opcodes.IASTORE));
                        }
                    }

                    public void load(MethodVisitor visitor, CompilationState state) throws CompilationFailedException {
                        state.assertMinimumStackSize(2);
                        TypeWrapper valueType = state.popStack();
                        TypeWrapper indexType = state.popStack();
                        TypeWrapper array = state.popStack();
                        if (!unit.reflectionUtils().isIntPrimitive(indexType)) {
                            throw new CompilationFailedException(suffix.getLineNumber(), "expected int but was: " + indexType);
                        }
                        if (!(array instanceof ArrayTypeWrapper)) {
                            state.pushStack(array);
                            state.pushStack(indexType);
                            ARRAY_ACCESS_MD.writeInvoke(visitor, state);
                        } else {
                            ArrayTypeWrapper concrete = (ArrayTypeWrapper) array;
                            this.type = concrete.getComponentType();
                            // just for the type coercion
                            state.pushStack(valueType);
                            unit.getCompilerManager().coerceToType(visitor, valueType, concrete.getComponentType(), state);
                            state.popStack();
                            visitor.visitInsn(concrete.getComponentType().toASMType().getOpcode(Opcodes.IALOAD));
                        }
                    }

                    public void prepare(MethodVisitor visitor, CompilationState state) throws CompilationFailedException {
                        TypeWrapper array = state.peekStack();
                        if (!(array instanceof ArrayTypeWrapper)) {
                            this.type = JavaClassWrapper.OBJECT_WRAPPER;
                        } else {
                            ArrayTypeWrapper concrete = (ArrayTypeWrapper) array;
                            this.type = concrete.getComponentType();
                        }
                        PrimarySuffixCompiler.pushArrayIndex(visitor, state, this.localUnit, suffix);
                    }

                    public void dup(MethodVisitor visitor, CompilationState state) throws CompilationFailedException {
                        state.assertMinimumStackSize(2);
                        TypeWrapper index = state.popStack();
                        TypeWrapper arrayType = state.popStack();
                        state.pushStack(arrayType);
                        state.pushStack(index);
                        state.pushStack(arrayType);
                        state.pushStack(index);
                        visitor.visitInsn(Opcodes.DUP2);
                    }

                    public TypeWrapper type() {
                        return this.type;
                    }

                    /**
                     * {@inheritDoc}
                     * 
                     * @see bsh.AssignmentNodeCompiler.AssignmentTarget#isLocal()
                     */
                    public boolean isLocal() {
                        return false;
                    }
                };
            } else {
                throw new CompilationFailedException(lhs.getLineNumber(), "cannot assign .class field");
            }
        }
        throw new CompilationAssertionFailed("unexpected type on lhs of assignment: " + lhs.getClass());
    }

    /**
     * @param exp {@link BSHPrimaryExpression}
     * @param currentScope {@link Scope}
     * @return boolean
     */
    private boolean isUndeclaredLocalVariable(BSHPrimaryExpression exp, Scope currentScope) {
        if ((exp.jjtGetNumChildren() == 1 && exp.getChild(0) instanceof BSHAmbiguousName)) {
            String text = ((BSHAmbiguousName) exp.getChild(0)).text;
            return "var".equals(text);
        } else {
            return false;
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @throws CompilationFailedException e
     */
    public TypeWrapper nodeResultType(Scope scope) throws CompilationFailedException {
        return this.unit.getNodeType(this.node.getChild(1), scope);
    }

    /**
     * @author takeshi
     */
    private static final class FieldAssignmentTarget implements AssignmentTarget {
        private final ObjectAccessor acc;

        /**
         * @param field {@link FieldDescriptor}
         */
        private FieldAssignmentTarget(FieldDescriptor field) {
            this.acc = new FieldObjectAcessor(field);
        }

        /**
         * {@inheritDoc}
         */
        public void doAssign(MethodVisitor visitor, CompilationState state) throws CompilationFailedException {
            this.acc.storeObject(visitor, state);
        }

        /**
         * {@inheritDoc}
         */
        public void load(MethodVisitor visitor, CompilationState state) throws CompilationFailedException {
            this.acc.loadObject(visitor, state);
        }

        /**
         * {@inheritDoc}
         */
        public void prepare(MethodVisitor visitor, CompilationState state) throws CompilationFailedException {
            this.acc.preStore(visitor, state);
        }

        /**
         * {@inheritDoc}
         */
        public void dup(MethodVisitor visitor, CompilationState state) throws CompilationFailedException {
            state.pushStack(state.peekStack());
            visitor.visitInsn(Opcodes.DUP);
        }

        /**
         * {@inheritDoc}
         */
        public TypeWrapper type() {
            return this.acc.type();
        }

        /**
         * {@inheritDoc}
         * 
         * @see bsh.AssignmentNodeCompiler.AssignmentTarget#isLocal()
         */
        public boolean isLocal() {
            return false;
        }
    }

    /**
     * @author takeshi
     */
    public static interface AssignmentTarget {

        /**
         * @param visitor {@link MethodVisitor}
         * @param state {@link CompilationState}
         * @throws CompilationFailedException e
         */
        void load(MethodVisitor visitor, CompilationState state) throws CompilationFailedException;

        /**
         * @param visitor {@link MethodVisitor}
         * @param state {@link CompilationState}
         * @throws CompilationFailedException e
         */
        void prepare(MethodVisitor visitor, CompilationState state) throws CompilationFailedException;

        /**
         * @param visitor {@link MethodVisitor}
         * @param state {@link CompilationState}
         * @throws CompilationFailedException e
         */
        void doAssign(MethodVisitor visitor, CompilationState state) throws CompilationFailedException;

        /**
         * @param visitor {@link MethodVisitor}
         * @param state {@link CompilationState}
         * @throws CompilationFailedException e
         */
        void dup(MethodVisitor visitor, CompilationState state) throws CompilationFailedException;

        /**
         * @return {@link TypeWrapper}
         */
        TypeWrapper type();

        /**
         * @return true se este {@link AssignmentTarget} for uma variavel local
         */
        boolean isLocal();
    }

}
